Foreword This document describes a typical base install for a Linux server. It is not intended to be a copy-and-paste guide.
You are still expected to understand what you are doing and why. The goal here is to streamline the process, reduce cognitive load, and avoid forgetting the small but important things, especially during late-night changes when mistakes are easiest to make.
Think of this as a guardrail, not a substitute for experience.
Who is this for? This document is written for system administrators.
If you are not a sysadmin, you should not use this as a guide for commercial systems or critical infrastructure.
That said, using it in a sandbox or lab environment to learn is perfectly reasonable. The fastest way to learn systems administration is still to break things safely and learn how to recover.
Environment
Host: <name>.ug.cfts.co
VM Name: isp-status.cfts.co
OS: Ubuntu Server 24.04 LTS (minimal install)
CPU/RAM: 4 vCPU / 4–12 GB RAM
Network: VMXNET3 on Core VLAN (172.16.198.x)
sda → OS (xxx GB SSD)
sdb → logs (200 GB U.2)
sdc → swap (25 GB U.2)
user sysops / <any_good_passowrd> (example only)
Basic Install
~$ sudo -i
~# apt-get update
~# apt-get upgrade
~# apt install -y curl
~# apt install -y openssh-server
~# apt install -y open-vm-tools
~# apt install -y iputils-ping
~# apt install -y iproute2
~# apt install -y htop
~# apt install -y iftop
~# apt install -y nano
~# apt install -y rsync
~# apt install -y tcpdump
~# reboot (that way you catch everything, you never know)
Make sure you IP is permanent!
~# nano /etc/cloud/cloud.cfg.d/90-installer-network.cfg
⚠️make sure it has the details you expect, or your next reboot might get interesting.
Default setup for SSH
~# nano /etc/ssh/sshd_config
Paste:
Port 4422
ListenAddress 0.0.0.0
PermitRootLogin no
PasswordAuthentication yes
KbdInteractiveAuthentication yes
UsePAM yes
PrintMotd no
PrintLastLog yes
AllowUsers sysops
⚠️ See: [[Dynamic-MOTD-StepByStep]] it will save you a bunch of time, no really! but if you want to chance it...
Database (Optional)
~# apt-get install -y locales
~# apt-get install -y postgresql
~# locale-gen en_US.UTF-8
Time Zone
~# timedatectl set-timezone Africa/Kampala
~# timedatectl set-ntp yes
~# timedatectl
SNMP
~# apt install -y snmpd snmp
~# systemctl status snmpd
~# nano /etc/snmp/snmpd.conf
Paste and alter accordingly:
# Bind SNMP only to loopback and LAN to prevent accidental exposure outside the core network
agentAddress udp:127.0.0.1:161,udp:172.16.198.26:161
# Read-only community, restricted to PRTG
rocommunity cfts 172.16.198.50
rocommunity cfts 127.0.0.1
# Basic system info
sysLocation HCI-SR1-UG
sysContact sysops@cfts.co
# Reduce noise
dontLogTCPWrappersConnects yes
~# sudo systemctl restart snmpd
⚠️Do not keep the default wide-open config.
Replace the contents with this minimal, explicit setup.
Testing SNMP
~# snmpwalk -v2c -c cfts localhost 1.3.6.1.2.1.1.5.0
⚠️ Pass criteria: you get a response (hostname).
Firewall
~# apt-get install -y ufw
~# ufw allow from 172.16.198.0/24 to any port 4422
~# ufw limit 4422/tcp comment 'rate limit SSH from outside the local subnet'
~# ufw allow 53/udp comment 'DNS'
~# ufw allow 53/tcp comment 'DNS'
~# ufw allow 443/udp comment 'QUIC HTTP/3'
~# ufw allow 443/tcp comment 'HTTPS'
~# ufw allow 80/tcp comment 'HTTP'
~# ufw allow 67/udp comment 'DHCP'
~# ufw allow from 172.16.198.50 to any port 161 proto udp comment 'SNMP'
~# ufw logging on
~# ufw enable
~# ufw status numbered
⚠️ Firewall rules are role-dependent. Review before enabling on non-edge hosts.
⚠️ When you add or take away rule remember to 'sudo ufw reload'
⚠️ Ping allowed by default via /etc/ufw/before.rules (echo-request). No explicit UFW rule required
Set the hostname (systemd-native way example)
~# hostnamectl set-hostname isp-status.cfts.co
~# hostnamectl
~# hostname -f
Update /etc/hosts (important)
~# nano /etc/hosts
127.0.0.1 localhost
127.0.1.1 localhost
172.16.198.26 isp-status.cfts.co isp-status
⚠️ Hostname set to match the service name
Filesystem & Logging (optional)
~# apt install logrotate
~# systemctl enable systemd-timesyncd
~# logrotate -f /etc/logrotate.conf
MOTD script 'Banner' (for when you first login) avoid the 3am confusion
~# nano /etc/update-motd.d/50-ops
Paste:
#!/usr/bin/env bash
set -euo pipefail
# --------- helpers ----------
hr() { printf '%*s\n' "$(tput cols 2>/dev/null || echo 80)" '' | tr ' ' '-'; }
HOST="$(hostname)"
FQDN="$(hostname -f 2>/dev/null || hostname)"
UPTIME="$(uptime -p 2>/dev/null || true)"
NOW="$(date)"
# Colors (only if stdout is a TTY)
if [ -t 1 ]; then
GREEN="\033[1;32m"
RESET="\033[0m"
else
GREEN=""
RESET=""
fi
# IPs (global scope, no docker/lo)
IPS="$(ip -br -4 addr show scope global 2>/dev/null | awk '{print $1": "$3}' | sed 's#/.*##' || true)"
IPS6="$(ip -br -6 addr show scope global 2>/dev/null | awk '{print $1": "$3}' | sed 's#/.*##' || true)"
# Memory summary
MEM="$(free -h | awk '/Mem:/ {print $3 " used / " $2 " total"}')"
SWP="$(free -h | awk '/Swap:/ {print $3 " used / " $2 " total"}')"
# Disk summary (root + top mounts)
ROOT_DISK="$(df -h / | awk 'NR==2 {print $3 " used / " $2 " total (" $5 " used) on " $1}')"
# Show top 6 mount points by % used (exclude tmpfs/overlay)
DISK_TOP="$(df -h -x tmpfs -x devtmpfs -x overlay 2>/dev/null \
| awk 'NR>1 {print $5"\t"$6"\t"$1}' \
| sort -hr | head -n 6 | awk '{printf " %-5s %-18s %s\n",$1,$2,$3}')"
# Last 5 logins (exclude reboots)
LAST5="$(last -n 5 -a 2>/dev/null | grep -vE 'reboot|wtmp begins' || true)"
# --------- output ----------
echo
hr
echo -e " ${GREEN}${FQDN} (${HOST})${RESET}"
echo " Time: $NOW"
[ -n "${UPTIME:-}" ] && echo " Uptime: $UPTIME"
hr
echo " Network:"
if [ -n "${IPS// }" ]; then
echo "$IPS" | sed 's/^/ - /'
else
echo " - (no IPv4 global addresses found)"
fi
if [ -n "${IPS6// }" ]; then
echo " IPv6:"
echo "$IPS6" | sed 's/^/ - /'
fi
hr
echo " Resources:"
echo " - Memory: $MEM"
echo " - Swap: $SWP"
echo " - Root: $ROOT_DISK"
echo " - Disks (top usage):"
echo "$DISK_TOP"
hr
echo " Last 5 logins:"
if [ -n "${LAST5// }" ]; then
echo "$LAST5" | sed 's/^/ /'
else
echo " (no login history found)"
fi
hr
echo
Make it executable and test:
~# chmod +x /etc/update-motd.d/50-ops
This is normally already enabled, but just in case:
~# grep -R "pam_motd" -n /etc/pam.d/sshd /etc/pam.d/login
Test it (without logging out)
~# run-parts /etc/update-motd.d/
Now to make sure it runs under SSH console (still twitchy, needs work)
~# nano /etc/pam.d/sshd
Look for something like this:
session optional pam_motd.so motd=/run/motd.dynamic
# session optional pam_motd.so noupdate
Symlink 'motd.dynamic' so there's no guessing
~# sudo ln -sf /run/motd.dynamic /etc/motd
⚠️ make sure /run/motd.dynamic - root can read ✔ all users can read ✔
- PAM reads the file as root anyway
⚠️ SSH + MOTD rule of thumb
- PAM must be enabled for dynamic MOTD
- Only one pam_motd.so line should be active
- /run/motd.dynamic is the single source of truth
A typical DNS setup would be:
DHCP:
Clients via DHCP use Technitium for everything — internal queries get bounced to AD2, external go upstream encrypted.
- Clients → DHCP → 172.16.198.49 (dns.cfts.co)
Keep AD DNS authoritative for cfts.local
- Primary DNS: 127.0.0.1 (or 172.16.198.15)
- Forwarders: 172.16.198.49 (Technitium)
A typical LAN setup DHCP or Fixed would be:
- IP: 172.16.198.xx
- MASK: 255.255.255.0
- Gateway: 172.16.198.254
- DNS: 172.16.198.49
Optional Hardening Fail2Ban: ⚠️ Fail2Ban is great - until it isn’t.
~# apt install -y fail2ban
~# nano /etc/fail2ban/jail.d/ssh-safe.conf
Paste:
[sshd]
enabled = true
port = 4422
backend = systemd
maxretry = 10
findtime = 10m
bantime = 10m
ignoreip = 127.0.0.1/8 172.16.198.0/24
Start & Check
~# systemctl enable --now fail2ban
~# systemctl restart fail2ban
~# fail2ban-client status sshd
⚠️ ~# fail2ban-client set sshd unbanip YOUR.IP.HERE
⚠️ Design note
Do not hard-code management ACLs to a single VM IP unless there is an out-of-band recovery path.
Prefer subnet-based ACLs or role-based access where possible.
⚠️ Ubuntu 24.04 note
Live installer requires ≥ 2 GB RAM and 4vCPU (4GB recommended).
1 GB may appear to hang during image extraction.